// This file is part of Keepass2Android, Copyright 2025 Philipp Crocoll.
//
//   Keepass2Android is free software: you can redistribute it and/or modify
//   it under the terms of the GNU General Public License as published by
//   the Free Software Foundation, either version 3 of the License, or
//   (at your option) any later version.
//
//   Keepass2Android is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU General Public License for more details.
//
//   You should have received a copy of the GNU General Public License
//   along with Keepass2Android.  If not, see <http://www.gnu.org/licenses/>.

using System;
using Android.Content;
using Javax.Crypto;
using Java.Security;
using Java.Lang;
using Android.Views.InputMethods;
using Android.App;
using Android.OS;
using Android.Security.Keystore;
using Android.Preferences;
using Android.Util;
using Android.Widget;
using AndroidX.Biometric;
using AndroidX.Fragment.App;
using Java.IO;
using Java.Security.Cert;
using Java.Util.Concurrent;
using Javax.Crypto.Spec;
using keepass2android;
using Exception = System.Exception;
using File = System.IO.File;

namespace keepass2android
{
  public interface IBiometricAuthCallback
  {
    void OnBiometricAuthSucceeded();
    void OnBiometricError(string toString);
    void OnBiometricAttemptFailed(string message);
  }

  public class BiometricModule
  {
    public AndroidX.Fragment.App.FragmentActivity Activity { get; set; }

    public BiometricModule(AndroidX.Fragment.App.FragmentActivity activity)
    {
      Activity = activity;
    }


    public KeyguardManager KeyguardManager
    {
      get
      {
        return (KeyguardManager)Activity.GetSystemService("keyguard");
      }
    }


    public KeyStore Keystore
    {
      get
      {
        try
        {
          return KeyStore.GetInstance("AndroidKeyStore");
        }
        catch (KeyStoreException e)
        {
          throw new RuntimeException("Failed to get an instance of KeyStore", e);
        }
      }
    }

    public KeyGenerator KeyGenerator
    {
      get
      {
        try
        {
          return KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, "AndroidKeyStore");
        }
        catch (NoSuchAlgorithmException e)
        {
          throw new RuntimeException("Failed to get an instance of KeyGenerator", e);
        }
        catch (NoSuchProviderException e)
        {
          throw new RuntimeException("Failed to get an instance of KeyGenerator", e);
        }
      }
    }

    public Cipher Cipher
    {
      get
      {
        try
        {
          return Cipher.GetInstance(KeyProperties.KeyAlgorithmAes + "/"
                                    + KeyProperties.BlockModeCbc + "/"
                                    + KeyProperties.EncryptionPaddingPkcs7);
        }
        catch (NoSuchAlgorithmException e)
        {
          throw new RuntimeException("Failed to get an instance of Cipher", e);
        }
        catch (NoSuchPaddingException e)
        {
          throw new RuntimeException("Failed to get an instance of Cipher", e);
        }
      }
    }

    public ISharedPreferences SharedPreferences
    {
      get { return PreferenceManager.GetDefaultSharedPreferences(Activity); }
    }

    public bool IsAvailable
    {
      get
      {
        return BiometricManager.From(Activity).CanAuthenticate() ==
               BiometricManager.BiometricSuccess;
      }
    }

    public bool IsHardwareAvailable
    {
      get
      {
        var result = BiometricManager.From(Activity).CanAuthenticate();
        Kp2aLog.Log("BiometricHardware available = " + result);
        return result == BiometricManager.BiometricSuccess
               || result == BiometricManager.BiometricErrorNoneEnrolled;
      }
    }
  }

  public abstract class BiometricCrypt : IBiometricIdentifier
  {
    protected const string FailedToInitCipher = "Failed to init Cipher";

    protected readonly string _keyId;

    protected Cipher _cipher;
    private CancellationSignal _cancellationSignal;
    protected BiometricPrompt.CryptoObject _cryptoObject;

    protected KeyStore _keystore;

    private BiometricPrompt _biometricPrompt;
    private FragmentActivity _activity;
    private BiometricAuthCallbackAdapter _biometricAuthCallbackAdapter;

    public BiometricCrypt(BiometricModule biometric, string keyId)
    {
      Kp2aLog.Log("FP: Create " + this.GetType().Name);
      _keyId = keyId;
      _cipher = biometric.Cipher;
      _keystore = biometric.Keystore;
      _activity = biometric.Activity;



    }

    public abstract bool Init();


    protected static string GetAlias(string keyId)
    {
      return "keepass2android." + keyId;
    }
    protected static string GetIvPrefKey(string prefKey)
    {
      return prefKey + "_iv";
    }

    public void StartListening(IBiometricAuthCallback callback)
    {
      _biometricAuthCallbackAdapter = new BiometricAuthCallbackAdapter(callback, _activity);
      StartListening(_biometricAuthCallbackAdapter);
    }

    public void StopListening()
    {
      Kp2aLog.Log("Fingerprint: StopListening " + (_biometricPrompt != null ? " having prompt " : " without prompt"));
      _biometricAuthCallbackAdapter?.IgnoreNextError();
      _biometricPrompt?.CancelAuthentication();
    }

    public bool HasUserInterface
    {
      get { return true; }

    }

    public void StartListening(BiometricPrompt.AuthenticationCallback callback)
    {

      Kp2aLog.Log("Fingerprint: StartListening ");

      var executor = Executors.NewSingleThreadExecutor();
      _biometricPrompt = new BiometricPrompt(_activity, executor, callback);

      BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
          .SetTitle(_activity.GetString(AppNames.AppNameResource))
          .SetSubtitle(_activity.GetString(Resource.String.unlock_database_title))
          .SetNegativeButtonText(_activity.GetString(Android.Resource.String.Cancel))
          .SetConfirmationRequired(false)
          .Build();


      _biometricPrompt.Authenticate(promptInfo, _cryptoObject);

    }

    public string Encrypt(string textToEncrypt)
    {
      Kp2aLog.Log("FP: Encrypting");
      return Base64.EncodeToString(_cipher.DoFinal(System.Text.Encoding.UTF8.GetBytes(textToEncrypt)), 0);
    }


    public void StoreEncrypted(string textToEncrypt, string prefKey, Context context)
    {
      var edit = PreferenceManager.GetDefaultSharedPreferences(context).Edit();
      StoreEncrypted(textToEncrypt, prefKey, edit);
      edit.Commit();
    }

    public void StoreEncrypted(string textToEncrypt, string prefKey, ISharedPreferencesEditor edit)
    {
      edit.PutString(prefKey, Encrypt(textToEncrypt));
      edit.PutString(GetIvPrefKey(prefKey), Base64.EncodeToString(CipherIv, 0));

    }


    private byte[] CipherIv
    {
      get { return _cipher.GetIV(); }
    }
  }

  public interface IBiometricIdentifier
  {
    bool Init();
    void StartListening(IBiometricAuthCallback callback);

    void StopListening();
    bool HasUserInterface { get; }
  }

  public class BiometricDecryption : BiometricCrypt
  {
    private readonly Context _context;
    private readonly byte[] _iv;


    public BiometricDecryption(BiometricModule biometric, string keyId, byte[] iv) : base(biometric, keyId)
    {
      _iv = iv;
    }

    public BiometricDecryption(BiometricModule biometric, string keyId, Context context, string prefKey)
        : base(biometric, keyId)
    {
      _context = context;
      _iv = Base64.Decode(PreferenceManager.GetDefaultSharedPreferences(context).GetString(GetIvPrefKey(prefKey), null), 0);
    }

    public static bool IsSetUp(Context context, string prefKey)
    {
      return PreferenceManager.GetDefaultSharedPreferences(context).GetString(GetIvPrefKey(prefKey), null) != null;
    }

    public override bool Init()
    {
      Kp2aLog.Log("FP: Init for Dec");
      try
      {
        _keystore.Load(null);
        var aliases = _keystore.Aliases();
        if (aliases == null)
        {
          Kp2aLog.Log("KS: no aliases");
        }
        else
        {
          while (aliases.HasMoreElements)
          {
            var o = aliases.NextElement();
            Kp2aLog.Log("alias: " + o?.ToString());
          }
          Kp2aLog.Log("KS: end aliases");

        }
        var key = _keystore.GetKey(GetAlias(_keyId), null);
        if (key == null)
          throw new Exception("Failed to init cipher for fingerprint Init: key is null");
        var ivParams = new IvParameterSpec(_iv);
        _cipher.Init(CipherMode.DecryptMode, key, ivParams);

        _cryptoObject = new BiometricPrompt.CryptoObject(_cipher);
        return true;
      }
      catch (KeyPermanentlyInvalidatedException e)
      {
        Kp2aLog.Log("FP: KeyPermanentlyInvalidatedException." + e.ToString());
        return false;
      }
      catch (KeyStoreException e)
      {
        throw new RuntimeException(FailedToInitCipher + " (keystore)", e);
      }
      catch (CertificateException e)
      {
        throw new RuntimeException(FailedToInitCipher + " (CertificateException)", e);
      }
      catch (UnrecoverableKeyException e)
      {
        throw new RuntimeException(FailedToInitCipher + " (UnrecoverableKeyException)", e);
      }
      catch (Java.IO.IOException e)
      {
        throw new RuntimeException(FailedToInitCipher + " (IOException)", e);
      }
      catch (NoSuchAlgorithmException e)
      {
        throw new RuntimeException(FailedToInitCipher + " (NoSuchAlgorithmException)", e);
      }
      catch (InvalidKeyException e)
      {
        throw new RuntimeException(FailedToInitCipher + " (InvalidKeyException)" + e.ToString(), e);
      }
    }


    public string Decrypt(string encryted)
    {
      Kp2aLog.Log("FP: Decrypting ");
      byte[] encryptedBytes = Base64.Decode(encryted, 0);
      return System.Text.Encoding.UTF8.GetString(_cipher.DoFinal(encryptedBytes));
    }

    public string DecryptStored(string prefKey)
    {
      string enc = PreferenceManager.GetDefaultSharedPreferences(_context).GetString(prefKey, null);
      return Decrypt(enc);
    }
  }

  public class BiometricEncryption : BiometricCrypt
  {

    private KeyGenerator _keyGen;


    public BiometricEncryption(BiometricModule biometric, string keyId) :
        base(biometric, keyId)
    {
      _keyGen = biometric.KeyGenerator;
      Kp2aLog.Log("FP: CreateKey ");
      CreateKey();
    }


    /// <summary>
    /// Creates a symmetric key in the Android Key Store which can only be used after the user 
    /// has authenticated with biometry.
    /// </summary>
    private void CreateKey()
    {
      try
      {
        _keystore.Load(null);
        KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(GetAlias(_keyId),
                KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
            .SetBlockModes(KeyProperties.BlockModeCbc)
            // Require the user to authenticate with biometry to authorize every use
            // of the key
            .SetEncryptionPaddings(KeyProperties.EncryptionPaddingPkcs7)
            .SetUserAuthenticationRequired(true);

        if ((int)Build.VERSION.SdkInt >= 24)
          builder.SetInvalidatedByBiometricEnrollment(true);

        _keyGen.Init(
            builder
            .Build());
        _keyGen.GenerateKey();
      }
      catch (NoSuchAlgorithmException e)
      {
        throw new RuntimeException(e);
      }
      catch (InvalidAlgorithmParameterException e)
      {
        throw new RuntimeException(e);
      }
      catch (CertificateException e)
      {
        throw new RuntimeException(e);
      }
      catch (Java.IO.IOException e)
      {
        throw new RuntimeException(e);
      }
      catch (System.Exception e)
      {
        Kp2aLog.LogUnexpectedError(e);
      }
    }

    public override bool Init()
    {
      Kp2aLog.Log("FP: Init for Enc ");
      try
      {
        _keystore.Load(null);
        var key = _keystore.GetKey(GetAlias(_keyId), null);
        _cipher.Init(CipherMode.EncryptMode, key);

        _cryptoObject = new BiometricPrompt.CryptoObject(_cipher);
        return true;
      }
      catch (KeyPermanentlyInvalidatedException)
      {
        return false;
      }
      catch (KeyStoreException e)
      {
        throw new RuntimeException(FailedToInitCipher, e);
      }
      catch (CertificateException e)
      {
        throw new RuntimeException(FailedToInitCipher, e);
      }
      catch (UnrecoverableKeyException e)
      {
        throw new RuntimeException(FailedToInitCipher, e);
      }
      catch (Java.IO.IOException e)
      {
        throw new RuntimeException(FailedToInitCipher, e);
      }
      catch (NoSuchAlgorithmException e)
      {
        throw new RuntimeException(FailedToInitCipher, e);
      }
      catch (InvalidKeyException e)
      {
        throw new RuntimeException(FailedToInitCipher, e);
      }
    }

  }

  public class BiometricAuthCallbackAdapter : BiometricPrompt.AuthenticationCallback
  {
    private readonly IBiometricAuthCallback _callback;
    private readonly Context _context;
    private bool _ignoreNextError;

    public BiometricAuthCallbackAdapter(IBiometricAuthCallback callback, Context context)
    {
      _callback = callback;
      _context = context;
    }


    public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result)
    {
      new Handler(Looper.MainLooper).Post(() => _callback.OnBiometricAuthSucceeded());
    }

    public override void OnAuthenticationError(int errorCode, ICharSequence errString)
    {
      if (_ignoreNextError)
      {
        _ignoreNextError = false;
        return;
      }
      new Handler(Looper.MainLooper).Post(() => _callback.OnBiometricError(errString.ToString()));
    }


    public override void OnAuthenticationFailed()
    {
      new Handler(Looper.MainLooper).Post(() => _callback.OnBiometricAttemptFailed(_context.Resources.GetString(Resource.String.fingerprint_not_recognized)));
    }

    public void IgnoreNextError()
    {
      _ignoreNextError = true;
    }
  }

}